MonoGame笔记(X6)Game State Management (三)

上一篇笔记中介绍了主菜单进入游戏再到退出游戏的大致流程. 接下来看一下ScreenManager的Update方法和Draw方法
Draw方法比较简单, 如果是Hidden状态, 则不绘制. Update方法复杂一点,它是用List当做Stack, 也是自顶向下更新. 而Draw如上一篇笔记所说, 是自底向上绘制.

Update方法中的两个变量: otherScreenHasFocus, coveredByOtherScreen比较关键. 并且这两个变量会传递给GameScreen的Update方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/// <summary>
/// Allows each screen to run logic.
/// </summary>
public override void Update(GameTime gameTime)
{
// Read the keyboard and gamepad.
input.Update();

// Make a copy of the master screen list, to avoid confusion if
// the process of updating one screen adds or removes others.
screensToUpdate.Clear();

foreach (GameScreen screen in screens)
screensToUpdate.Add(screen);

bool otherScreenHasFocus = !Game.IsActive;
bool coveredByOtherScreen = false;

// Loop as long as there are screens waiting to be updated.
while (screensToUpdate.Count > 0)
{
// Pop the topmost screen off the waiting list.
GameScreen screen = screensToUpdate[screensToUpdate.Count - 1];

screensToUpdate.RemoveAt(screensToUpdate.Count - 1);

// Update the screen.
screen.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);

if (screen.ScreenState == ScreenState.TransitionOn ||
screen.ScreenState == ScreenState.Active)
{
// If this is the first active screen we came across,
// give it a chance to handle input.
if (!otherScreenHasFocus)
{
screen.HandleInput(input);

otherScreenHasFocus = true;
}

// If this is an active non-popup, inform any subsequent
// screens that they are covered by it.
if (!screen.IsPopup)
coveredByOtherScreen = true;
}
}

// Print debug trace?
if (traceEnabled)
TraceScreens();
}

/// <summary>
/// Tells each screen to draw itself.
/// </summary>
public override void Draw(GameTime gameTime)
{
foreach (GameScreen screen in screens)
{
if (screen.ScreenState == ScreenState.Hidden)
continue;

screen.Draw(gameTime);
}
}

otherScreenHasFocus用来控制输入焦点;只有最顶上的那个GameScreen可以接收输入, 之后的GameScreen都不能接收输入.

coveredByOtherScreen用来控制是否绘制该GameScreen. 例如游戏画面被主菜单盖住, GamePlayScreen就不需要再被绘制. 前提是盖住的那个GameScreen不是Popup弹窗. MessageBoxScreen属于Popup弹窗.

GameScreen的Update方法对coveredByOtherScreen进行判断. 如果为真, 该GameScreen就需要淡出, 直至完全隐藏.
淡出时仍旧需要绘制.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/// <summary>
/// Allows the screen to run logic, such as updating the transition position.
/// Unlike HandleInput, this method is called regardless of whether the screen
/// is active, hidden, or in the middle of a transition.
/// </summary>
public virtual void Update(GameTime gameTime, bool otherScreenHasFocus,
bool coveredByOtherScreen)
{
this.otherScreenHasFocus = otherScreenHasFocus;

if (isExiting)
{
// If the screen is going away to die, it should transition off.
screenState = ScreenState.TransitionOff;

if (!UpdateTransition(gameTime, transitionOffTime, 1))
{
// When the transition finishes, remove the screen.
ScreenManager.RemoveScreen(this);
}
}
else if (coveredByOtherScreen)
{
// If the screen is covered by another, it should transition off.
if (UpdateTransition(gameTime, transitionOffTime, 1))
{
// Still busy transitioning.
screenState = ScreenState.TransitionOff;
}
else
{
// Transition finished!
screenState = ScreenState.Hidden;
}
}
else
{
// Otherwise the screen should transition on and become active.
if (UpdateTransition(gameTime, transitionOnTime, -1))
{
// Still busy transitioning.
screenState = ScreenState.TransitionOn;
}
else
{
// Transition finished!
screenState = ScreenState.Active;
}
}
}

这种做法还是很巧妙的. 顶上GameScreen借助这种方式”通知”下面的GameScreen. 而且, 顶上的GameScreen如果是TransitionOn淡入, 底下的GameScreen就得到TransitionOff淡出的”通知”, 两者一前一后几乎同时发生, 视觉效果不错.

接下来看看MenuScreen与MenuEntry的关系.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// Updates the menu.
/// </summary>
public override void Update(GameTime gameTime, bool otherScreenHasFocus,
bool coveredByOtherScreen)
{
base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);

// Update each nested MenuEntry object.
for (int i = 0; i < menuEntries.Count; i++)
{
bool isSelected = IsActive && (i == selectedEntry);

menuEntries[i].Update(this, isSelected, gameTime);
}
}

MenuEntry有自己的Update和Draw方法. MenuEntry自己控制Selected与Unselected时的状态效果. MenuEntry还暴露了一个Selected事件, 供MenuScreen监听. 但MenuEntry是否被选中, 是在MenuScreen的HandleInput中进行判断. 不是在MenuEntry内部. selectedEntry是由MenuScreen控制的, 具体有多少个Entry只有MenuScreen知晓

方向键移动, Enter键选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/// <summary>
/// Responds to user input, changing the selected entry and accepting
/// or cancelling the menu.
/// </summary>
public override void HandleInput(InputState input)
{
// Move to the previous menu entry?
if (input.IsMenuUp(ControllingPlayer))
{
selectedEntry--;

if (selectedEntry < 0)
selectedEntry = menuEntries.Count - 1;
}

// Move to the next menu entry?
if (input.IsMenuDown(ControllingPlayer))
{
selectedEntry++;

if (selectedEntry >= menuEntries.Count)
selectedEntry = 0;
}

// Accept or cancel the menu? We pass in our ControllingPlayer, which may
// either be null (to accept input from any player) or a specific index.
// If we pass a null controlling player, the InputState helper returns to
// us which player actually provided the input. We pass that through to
// OnSelectEntry and OnCancel, so they can tell which player triggered them.
PlayerIndex playerIndex;

if (input.IsMenuSelect(ControllingPlayer, out playerIndex))
{
OnSelectEntry(selectedEntry, playerIndex);
}
else if (input.IsMenuCancel(ControllingPlayer, out playerIndex))
{
OnCancel(playerIndex);
}
}

/// <summary>
/// Handler for when the user has chosen a menu entry.
/// </summary>
protected virtual void OnSelectEntry(int entryIndex, PlayerIndex playerIndex)
{
menuEntries[entryIndex].OnSelectEntry(playerIndex);
}

MenuEntry

1
2
3
4
5
6
7
8
/// <summary>
/// Method for raising the Selected event.
/// </summary>
protected internal virtual void OnSelectEntry(PlayerIndex playerIndex)
{
if (Selected != null)
Selected(this, new PlayerIndexEventArgs(playerIndex));
}

其他

BackgroundScreen的LoadContent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/// <summary>
/// Loads graphics content for this screen. The background texture is quite
/// big, so we use our own local ContentManager to load it. This allows us
/// to unload before going from the menus into the game itself, wheras if we
/// used the shared ContentManager provided by the Game class, the content
/// would remain loaded forever.
/// </summary>
public override void LoadContent()
{
if (content == null)
content = new ContentManager(ScreenManager.Game.Services, "Content");

backgroundTexture = content.Load<Texture2D>("background");
}

/// <summary>
/// Updates the background screen. Unlike most screens, this should not
/// transition off even if it has been covered by another screen: it is
/// supposed to be covered, after all! This overload forces the
/// coveredByOtherScreen parameter to false in order to stop the base
/// Update method wanting to transition off.
/// </summary>
public override void Update(GameTime gameTime, bool otherScreenHasFocus,
bool coveredByOtherScreen)
{
base.Update(gameTime, otherScreenHasFocus, false);
}